Skip to content

fix: discord bug because no wallet when there's no session#1762

Merged
edisontim merged 7 commits intomainfrom
feat/fix-discord-bug-no-session
Jun 23, 2025
Merged

fix: discord bug because no wallet when there's no session#1762
edisontim merged 7 commits intomainfrom
feat/fix-discord-bug-no-session

Conversation

@edisontim
Copy link
Contributor

@edisontim edisontim commented Jun 17, 2025

Depends on #1722

@vercel
Copy link

vercel bot commented Jun 17, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
controller-example-next ✅ Ready (Inspect) Visit Preview Jun 23, 2025 8:00am
keychain ✅ Ready (Inspect) Visit Preview Jun 23, 2025 8:00am
keychain-storybook ✅ Ready (Inspect) Visit Preview Jun 23, 2025 8:00am
profile ✅ Ready (Inspect) Visit Preview Jun 23, 2025 8:00am

@claude
Copy link
Contributor

claude bot commented Jun 17, 2025

Claude finished @edisontim's task —— View job


PR Review: Discord Session Bug Fix

This PR refactors Turnkey wallet integration and session management to address Discord authentication issues. Here's my comprehensive review:

Strengths

Architecture Improvements:

  • Clear separation of concerns: Moving Turnkey from controller to keychain package improves modularity
  • Better UI organization: Sessions and signers are now properly separated into dedicated components
  • Enhanced connection logic: The connection hook now handles Discord-specific embedded wallet detection (lines 217-267 in connection.ts)

Code Quality:

  • Error handling: Good error handling in Turnkey wallet implementation with detailed error messages
  • Type safety: Proper TypeScript usage throughout the codebase
  • Clean interfaces: Well-defined interfaces like SessionCardProps and WalletsContextValue

⚠️ Security Concerns

High Priority:

  • 🔴 Nonce validation: In turnkey.ts:99-106, nonce validation could be bypassed if tknonce is undefined - consider making this validation more strict
  • 🔴 Token handling: Raw JWT tokens are stored and passed around (line 89 in turnkey_utils.ts) - ensure these are properly secured
  • 🔴 Popup window: The authentication popup (line 390-396 in turnkey.ts) doesn't validate the returned window, potential for clickjacking

Medium Priority:

  • 🟡 Multiple suborgs: Dev environment allows multiple suborgs (turnkey_utils.ts:164-169) but production restricts this - ensure consistent behavior
  • 🟡 Address validation: Account mismatch checking (lines 147-151 in turnkey.ts) uses BigInt comparison which may not handle edge cases properly

🐛 Potential Bugs

Critical:

  • 🔴 Race condition: In wallets.tsx:66-87, the polling interval could cause memory leaks if component unmounts during async operations
  • 🔴 Error propagation: In KeychainWallets.signMessage(), embedded wallet failures may not properly bubble up errors

Medium:

  • 🟡 Timeout handling: getTurnkeyIframeClient() timeout implementation (lines 362-371) could leave hanging promises
  • 🟡 DOM manipulation: Direct DOM manipulation for iframe cleanup (lines 38-49 in turnkey.ts) might conflict with React's lifecycle

📈 Performance Issues

Polling Concerns:

  • 🟡 Aggressive polling: 300ms polling interval for wallet detection may be too frequent for production
  • 🟡 Memory usage: embeddedWalletsByAddress Map grows but never cleans up removed wallets

Suggestions:

// Consider debounced wallet detection
const detectWalletsDebounced = useMemo(
  () => debounce(detectWallets, 1000),
  [detectWallets]
);

🧪 Test Coverage Gaps

Missing Tests:

  • Turnkey authentication flow: No tests for the complex Auth0 + Turnkey integration
  • Session management: Session creation/deletion logic isn't tested
  • Error scenarios: Edge cases like network failures, malformed tokens not covered
  • Connection logic: The Discord-specific embedded wallet detection needs comprehensive testing

Test Suggestions:

  • Unit tests for TurnkeyWallet class methods
  • Integration tests for social authentication flow
  • E2E tests for session management UI

🔧 Code Quality Improvements

Refactoring Opportunities:

  1. Extract constants: Magic numbers like 10_000 timeout and 300 polling interval should be configurable
  2. Simplify complex methods: useConnectionValue() is quite large (598 lines) - consider breaking into smaller hooks
  3. Add JSDoc: Complex methods like authenticateToTurnkey() need better documentation

Example Fix:

// Instead of magic numbers
const TURNKEY_TIMEOUT_MS = 10_000;
const WALLET_POLL_INTERVAL_MS = 300;

📋 Action Items

High Priority:

  1. ✅ Strengthen nonce validation in Auth0 flow
  2. ✅ Add cleanup for wallet polling intervals
  3. ✅ Implement proper error boundaries for Turnkey operations
  4. ✅ Add comprehensive tests for authentication flow

Medium Priority:

  1. 📝 Extract configuration constants
  2. 📝 Add JSDoc documentation for complex methods
  3. 📝 Consider implementing wallet detection caching
  4. 📝 Review iframe lifecycle management

🎯 Overall Assessment

This is a solid refactoring that addresses the Discord session bug through better architecture. The code quality is generally good with proper TypeScript usage and error handling. However, the security concerns around token handling and the lack of test coverage for critical authentication flows are notable issues that should be addressed before merging.

Recommendation: Request Changes - Address security concerns and add tests for critical paths.


@claude
Copy link
Contributor

claude bot commented Jun 17, 2025

Claude finished @edisontim's task —— View job


PR Review: Discord Session Bug Fix

Overview

This PR refactors the Turnkey wallet integration by moving it from the controller package to the keychain package, addressing Discord authentication session issues. The changes include significant architectural improvements and code reorganization.

Strengths

Architecture Improvements:

  • Clear separation of concerns: Moving Turnkey from controller to keychain package improves modularity
  • Better error handling: Comprehensive error handling with detailed error messages throughout the codebase
  • Enhanced wallet detection: Improved polling mechanism for detecting wallet changes
  • Type safety: Good TypeScript usage with proper interfaces and type definitions

Code Quality:

  • Clean interfaces: Well-defined interfaces like WalletsContextValue and KeychainWallets
  • Consistent patterns: Standardized error response format across wallet operations
  • Proper cleanup: Cleanup patterns for intervals and DOM elements

🔴 Critical Security Concerns

Authentication Flow Vulnerabilities (High Priority):

  • Nonce validation bypass: In turnkey_utils.ts:95-96, if tknonce is undefined, validation is silently skipped - this could allow replay attacks
if (!decodedToken.tknonce) {
  return undefined; // This bypasses nonce validation!
}
  • JWT token exposure: Raw JWT tokens are stored and passed around (turnkey_utils.ts:89) without proper sanitization
  • Popup window vulnerability: The authentication popup in turnkey.ts:390-396 doesn't validate the returned window, creating potential for clickjacking attacks

DOM Manipulation Risks (Medium Priority):

  • Direct DOM manipulation: Lines 38-49 in turnkey.ts directly manipulate DOM elements outside React's lifecycle, which could cause memory leaks or conflicts
  • Iframe lifecycle: Iframe cleanup logic might not handle all edge cases, potentially leaving security vulnerabilities

🐛 Potential Bugs

Critical Issues:

  • Memory leaks: In wallets.tsx:66-87, the polling interval could cause memory leaks if the component unmounts during async operations
  • Race conditions: Multiple simultaneous calls to getTurnkeyIframeClient() could result in undefined behavior
  • Promise timeout handling: Lines 363-371 in turnkey.ts - timeout implementation could leave hanging promises

Medium Issues:

  • Address validation: turnkey.ts:147-151 uses BigInt comparison for address matching, which may not handle edge cases properly
  • Error propagation: Failed wallet operations may not properly bubble up errors to the UI layer

Performance Issues

Polling Concerns:

  • Aggressive polling: 300ms polling interval in wallets.tsx:85 may be too frequent for production environments
  • Memory usage: embeddedWalletsByAddress Map grows but never cleans up removed wallets
  • Inefficient wallet detection: Current implementation re-detects all wallets every 300ms instead of using event-based detection

Optimization Suggestions:

// Consider debounced wallet detection
const detectWalletsDebounced = useMemo(
  () => debounce(detectWallets, 1000),
  [detectWallets]
);

🧪 Test Coverage Gaps

Missing Critical Tests:

  • Authentication flow: No tests for Auth0 + Turnkey integration
  • Session management: Session creation/deletion logic untested
  • Error scenarios: Edge cases like network failures, malformed tokens
  • Security boundaries: Nonce validation, token handling not covered
  • Wallet detection: Polling logic and cleanup not tested

🔧 Code Quality Improvements

Magic Numbers and Configuration:

// Extract these constants
const TURNKEY_TIMEOUT_MS = 10_000;
const WALLET_POLL_INTERVAL_MS = 300;
const IFRAME_PUBLIC_KEY_POLL_INTERVAL_MS = 200;

Method Complexity:

  • TurnkeyWallet.connect() method is 86 lines - consider breaking into smaller methods
  • useConnection() hook likely needs refactoring (mentioned as 598 lines in context)

Documentation:

  • Complex authentication flow needs JSDoc documentation
  • Error handling strategies should be documented
  • Security considerations should be documented in code

🔒 Security Recommendations

High Priority Fixes:

  1. Strengthen nonce validation:
if (!decodedToken.tknonce) {
  throw new Error("Missing nonce in token - potential replay attack");
}
  1. Add popup validation:
const popup = openPopup("");
if (!popup || popup.closed) {
  throw new Error("Failed to open authentication popup");
}
  1. Sanitize error messages: Ensure error messages don't leak sensitive information

📋 Action Items

High Priority:

  • Fix nonce validation bypass vulnerability
  • Add proper popup window validation
  • Implement wallet polling cleanup to prevent memory leaks
  • Add comprehensive tests for authentication flow

Medium Priority:

  • Extract configuration constants
  • Add JSDoc documentation for complex methods
  • Implement event-based wallet detection instead of polling
  • Review and optimize DOM manipulation patterns

Low Priority:

  • Consider implementing wallet detection caching
  • Optimize error handling consistency
  • Add performance monitoring for polling operations

🎯 Overall Assessment

This is a solid architectural refactoring that addresses the Discord session bug through better separation of concerns. The code quality is generally good with proper TypeScript usage. However, the security vulnerabilities around nonce validation and the lack of comprehensive test coverage for critical authentication flows are concerning.

Recommendation: Request Changes - The security issues, particularly around authentication flow validation, should be addressed before merging. The memory leak potential and missing test coverage also need attention.


@claude
Copy link
Contributor

claude bot commented Jun 18, 2025

Claude finished @edisontim's task —— View job


PR Review: Discord Session Bug Fix

Review Tasks:

  • Read and analyze changed files
  • Review architectural changes (controller → keychain)
  • Assess code quality and best practices
  • Identify potential bugs and security issues
  • Evaluate performance considerations
  • Check test coverage gaps
  • Provide comprehensive feedback

Overview

This PR refactors the Turnkey social wallet integration by moving it from the controller package to the keychain package. The changes address Discord authentication session issues by implementing a cleaner separation of concerns and better embedded wallet management.

Strengths

Architectural Improvements

  • Clear separation of concerns: Moving Turnkey from controller to keychain improves security by keeping sensitive operations in the sandboxed iframe
  • Better embedded wallet management: The new KeychainWallets service provides a clean interface for managing embedded vs external wallets
  • Improved connection detection: The enhanced connection hook now properly detects Discord-specific embedded wallets and associates them with existing signers

Code Quality

  • Strong TypeScript usage: Proper interfaces and type safety throughout
  • Consistent error handling: Good error handling patterns with detailed error messages
  • Clean API design: The new social authentication hook has a simple, intuitive interface

🔴 Critical Security Concerns

Authentication Flow Vulnerabilities

1. Nonce Validation Bypass (turnkey_utils.ts:94-96)

const decodedToken = jwtDecode<DecodedIdToken>(oidcTokenString);
if (!decodedToken.tknonce) {
  return undefined; // ⚠️ CRITICAL: Silently bypasses nonce validation!
}

Risk: If tknonce is missing from the JWT, validation is bypassed entirely, potentially allowing replay attacks.
Fix: Should throw an error instead of returning undefined.

2. Popup Window Security (turnkey.ts:390-396)

const openPopup = (url: string) => {
  return window.open(url, "auth0:authorize:popup", `resizable,scrollbars=no,status=1`);
};

Risk: No validation that popup opened successfully, creating potential for clickjacking.
Fix: Check if popup is null/blocked and handle appropriately.

3. Raw JWT Token Exposure (turnkey_utils.ts:89)

const oidcTokenString = tokenClaims.__raw;

Risk: Raw JWT tokens are passed around without sanitization, potentially exposing sensitive claims.

DOM Manipulation Risks

4. Direct DOM Operations (turnkey.ts:38-49)

const turnkeyIframe = document.getElementById("turnkey-iframe-container");
if (turnkeyIframe) {
  document.body.removeChild(turnkeyIframe); // ⚠️ Direct DOM manipulation
}

Risk: Direct DOM manipulation outside React's lifecycle could cause memory leaks or conflicts.

🐛 Potential Bugs

Memory Management Issues

1. Polling Interval Cleanup (wallets.tsx:66-87)

const intervalId = setInterval(async () => {
  const detected = await parent.externalDetectWallets();
  // ... processing
}, 300);

Risk: If component unmounts during async operation, interval continues running.
Fix: Add proper cleanup in useEffect return.

2. Promise Timeout Implementation (turnkey.ts:363-371)

const timeoutId = setTimeout(() => {
  throw new Error("Timeout waiting for promise"); // ⚠️ Throws in timeout callback
}, timeoutMs);

Risk: Throwing error in setTimeout callback doesn't reject the promise.
Fix: Use Promise.race with a rejected timeout promise.

Type Safety Issues

3. Address Comparison (turnkey.ts:147-149)

if ("address" in signupOrLogin && BigInt(signupOrLogin.address) !== BigInt(turnkeyAddress)) {
  throw new Error("Account mismatch");
}

Risk: BigInt() constructor can throw for invalid input, causing unexpected errors.
Fix: Add try-catch or validate input format first.

Performance Concerns

Aggressive Polling

  • 300ms polling interval for wallet detection may be too frequent for production
  • No cleanup for embeddedWalletsByAddress Map - grows indefinitely
  • 200ms iframe polling creates additional load

Optimization Suggestions:

// Use exponential backoff or event-based detection
const INITIAL_POLL_INTERVAL = 300;
const MAX_POLL_INTERVAL = 3000;

// Add cleanup for embedded wallets map
private cleanupStaleWallets() {
  // Remove wallets that are no longer connected
}

🧪 Test Coverage Gaps

Missing Critical Tests

  • Authentication flow: No tests for Auth0 + Turnkey integration
  • Security boundaries: Nonce validation, token handling not tested
  • Error scenarios: Network failures, malformed tokens, popup blocking
  • Memory management: Polling cleanup, DOM manipulation lifecycle
  • Edge cases: Multiple suborgs, wallet creation failures

Recommended Test Coverage

describe('TurnkeyWallet', () => {
  it('should reject invalid nonce in JWT token')
  it('should handle popup blocking gracefully')
  it('should cleanup DOM elements on destruction')
  it('should handle network failures during authentication')
  it('should properly cleanup polling intervals')
})

🔧 Code Quality Improvements

Extract Configuration Constants

// Instead of magic numbers throughout codebase
const CONFIG = {
  TURNKEY_TIMEOUT_MS: 10_000,
  WALLET_POLL_INTERVAL_MS: 300,
  IFRAME_POLL_INTERVAL_MS: 200,
  MAX_WALLETS_PROD: 1,
} as const;

Method Complexity

  • TurnkeyWallet.connect() method is 86 lines - consider breaking into smaller methods
  • pollIframePublicKey() could be simplified with async/await patterns

Error Message Consistency

Some error messages expose internal details while others are generic. Consider standardizing error responses for security.

📋 Recommended Actions

High Priority (Security)

  1. Fix nonce validation bypass - throw error instead of returning undefined
  2. Add popup validation - check if popup opened successfully
  3. Implement proper promise timeout - use Promise.race pattern
  4. Add memory leak prevention - cleanup polling intervals properly

Medium Priority (Reliability)

  1. Add comprehensive error boundaries for authentication flows
  2. Implement exponential backoff for polling operations
  3. Add wallet map cleanup to prevent memory growth
  4. Extract configuration constants from hardcoded values

Low Priority (Quality)

  1. Add JSDoc documentation for complex authentication flows
  2. Consider event-based wallet detection instead of polling
  3. Standardize error message formats across the codebase

🎯 Overall Assessment

This is a solid architectural refactoring that improves separation of concerns and addresses the Discord session bug. The code quality is generally good with proper TypeScript usage and error handling patterns.

However, the critical security vulnerabilities around nonce validation and memory management issues with polling must be addressed before merging. The lack of comprehensive test coverage for authentication flows is also a significant concern.

Recommendation: Request Changes - Address security vulnerabilities and add proper cleanup mechanisms before merging.


@claude
Copy link
Contributor

claude bot commented Jun 18, 2025

Claude finished @edisontim's task —— View job


PR Review: Discord Session Bug Fix

I'll provide a comprehensive review of this PR focusing on the requested areas. Let me work through this systematically.

Review Tasks:

  • Read and analyze the changed files
  • Review architectural changes (controller → keychain migration)
  • Assess code quality and best practices
  • Identify potential bugs and security issues
  • Evaluate performance considerations
  • Check test coverage gaps
  • Provide comprehensive feedback

Overview

This PR addresses Discord authentication session issues by refactoring the Turnkey social wallet integration. The main change moves Turnkey implementation from the controller package to the keychain package, improving security by keeping sensitive operations in the sandboxed iframe environment.

Key Changes:

  • Architecture: Moved Turnkey from packages/controller/src/wallets/turnkey/ to packages/keychain/src/wallets/social/
  • Discord Detection: Added logic to automatically detect and link existing Discord signers to embedded Turnkey wallets
  • Code Organization: Simplified social authentication with cleaner separation of concerns

Strengths

Architectural Improvements

  • Enhanced Security: Moving Turnkey to keychain keeps sensitive operations in the sandboxed iframe
  • Better Separation: Clear distinction between external wallets (controller) and embedded wallets (keychain)
  • Discord Integration: Smart detection logic automatically associates Discord signers with their embedded wallets (connection.ts:217-267)

Code Quality

  • Type Safety: Good TypeScript usage with proper interfaces
  • Error Handling: Consistent error response patterns across wallet operations
  • Clean API: The useSocialAuthentication hook provides a simple interface

🔴 Critical Security Concerns

1. Nonce Validation Bypass (turnkey_utils.ts:94-96)

const decodedToken = jwtDecode<DecodedIdToken>(oidcTokenString);
if (!decodedToken.tknonce) {
  return undefined; // ⚠️ CRITICAL: Silently bypasses nonce validation!
}

Risk: If tknonce is missing from JWT, validation is bypassed entirely, potentially allowing replay attacks.

Fix: Should throw an error instead of returning undefined:

if (!decodedToken.tknonce) {
  throw new Error("Missing nonce in token - potential security risk");
}

2. Promise Timeout Implementation (turnkey.ts:363-371)

const timeoutId = setTimeout(() => {
  throw new Error("Timeout waiting for promise"); // ⚠️ BUG: Doesn't reject promise
}, timeoutMs);

Risk: Throwing error in setTimeout callback doesn't reject the promise, leading to hanging promises.

Fix: Use Promise.race pattern:

private async getPromiseResult<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
  const timeoutPromise = new Promise<never>((_, reject) => {
    setTimeout(() => reject(new Error("Timeout waiting for promise")), timeoutMs);
  });
  return Promise.race([promise, timeoutPromise]);
}

3. Popup Window Security (turnkey.ts:390-396)

const openPopup = (url: string) => {
  return window.open(url, "auth0:authorize:popup", `resizable,scrollbars=no,status=1`);
};

Risk: No validation that popup opened successfully, creating potential for clickjacking.

Fix: Add validation:

const openPopup = (url: string) => {
  const popup = window.open(url, "auth0:authorize:popup", `resizable,scrollbars=no,status=1`);
  if (!popup || popup.closed) {
    throw new Error("Failed to open authentication popup - may be blocked");
  }
  return popup;
};

4. Address Validation Risk (turnkey.ts:147-149)

if ("address" in signupOrLogin && BigInt(signupOrLogin.address) !== BigInt(turnkeyAddress)) {
  throw new Error("Account mismatch");
}

Risk: BigInt() constructor can throw for invalid input, causing unexpected errors.

🐛 Potential Bugs

1. Memory Leak Risk (wallets.tsx:66-87)

const intervalId = setInterval(async () => {
  const detected = await parent.externalDetectWallets();
  // ... processing
}, 300);

Risk: If component unmounts during async operation, interval continues running.

Fix: Add proper cleanup:

useEffect(() => {
  let mounted = true;
  const intervalId = setInterval(async () => {
    if (!mounted) return;
    const detected = await parent.externalDetectWallets();
    if (mounted) {
      // ... processing
    }
  }, 300);
  
  return () => {
    mounted = false;
    clearInterval(intervalId);
  };
}, [parent, wallets]);

2. DOM Manipulation Issues (turnkey.ts:38-49)

const turnkeyIframe = document.getElementById("turnkey-iframe-container");
if (turnkeyIframe) {
  document.body.removeChild(turnkeyIframe); // Direct DOM manipulation
}

Risk: Direct DOM manipulation outside React's lifecycle could cause conflicts.

Performance Issues

Aggressive Polling

  • 300ms polling interval (wallets.tsx:85) may be too frequent for production
  • 200ms iframe polling (turnkey.ts:316) adds additional load
  • No cleanup for embeddedWalletsByAddress Map - grows indefinitely

Recommendations:

// Use exponential backoff
const INITIAL_POLL_INTERVAL = 300;
const MAX_POLL_INTERVAL = 3000;

// Add map cleanup
private cleanupStaleWallets() {
  // Remove wallets that are no longer connected
}

🧪 Test Coverage Gaps

Missing Critical Tests

  • Authentication flow: No tests for Auth0 + Turnkey integration
  • Security boundaries: Nonce validation, token handling not tested
  • Error scenarios: Network failures, popup blocking, malformed tokens
  • Memory management: Polling cleanup, DOM lifecycle
  • Discord detection: Automatic wallet linking logic

Recommended Test Coverage

describe('TurnkeyWallet', () => {
  it('should reject missing nonce in JWT token');
  it('should handle popup blocking gracefully');
  it('should cleanup DOM elements on destruction');
  it('should timeout promises properly');
  it('should handle network failures during authentication');
});

describe('Discord Integration', () => {
  it('should detect existing Discord signers');
  it('should link signers to embedded wallets');
});

🔧 Code Quality Improvements

Extract Configuration Constants

const CONFIG = {
  TURNKEY_TIMEOUT_MS: 10_000,
  WALLET_POLL_INTERVAL_MS: 300,
  IFRAME_POLL_INTERVAL_MS: 200,
} as const;

Simplify Complex Methods

  • TurnkeyWallet.connect() is 86 lines - consider breaking into smaller methods
  • pollIframePublicKey() could use async/await patterns more consistently

📋 Action Items

High Priority (Security & Bugs)

  1. Fix nonce validation bypass - throw error instead of returning undefined
  2. Fix promise timeout implementation - use Promise.race pattern
  3. Add popup validation - check if popup opened successfully
  4. Add memory leak prevention - proper cleanup for polling intervals

Medium Priority (Performance & Reliability)

  1. Implement exponential backoff for polling operations
  2. Add wallet map cleanup to prevent memory growth
  3. Extract configuration constants from hardcoded values
  4. Add comprehensive error boundaries for authentication flows

Low Priority (Quality)

  1. Add JSDoc documentation for complex authentication flows
  2. Consider event-based wallet detection instead of polling
  3. Standardize error message formats

🎯 Overall Assessment

This is a solid architectural improvement that enhances security by moving sensitive operations to the keychain iframe. The Discord detection logic is well-implemented and addresses the core session issue.

However, the critical security vulnerabilities around nonce validation and promise handling must be addressed before merging. The memory management issues with polling and the lack of test coverage for authentication flows are also significant concerns.

Recommendation: Request Changes - Address security vulnerabilities and implement proper cleanup mechanisms before merging.


@claude
Copy link
Contributor

claude bot commented Jun 18, 2025

Claude finished @edisontim's task —— View job


PR Review: Discord Session Bug Fix

I'm conducting a comprehensive review of this PR. Let me work through this systematically.

Review Tasks:

  • Read and analyze the changed files
  • Review architectural changes (controller → keychain migration)
  • Assess code quality and best practices
  • Identify potential bugs and security issues
  • Evaluate performance considerations
  • Check test coverage gaps
  • Provide comprehensive feedback

Overview

This PR addresses Discord authentication session issues by refactoring the Turnkey social wallet integration. The main change moves Turnkey implementation from the controller package to the keychain package, improving security by keeping sensitive operations in the sandboxed iframe environment.

Key Changes:

  • Architecture: Moved Turnkey from packages/controller/src/wallets/turnkey/ to packages/keychain/src/wallets/social/
  • Discord Detection: Added logic to automatically detect and link existing Discord signers to embedded Turnkey wallets
  • Code Organization: Simplified social authentication with cleaner separation of concerns

Strengths

Architectural Improvements

  • Enhanced Security: Moving Turnkey to keychain keeps sensitive operations in the sandboxed iframe
  • Better Separation: Clear distinction between external wallets (controller) and embedded wallets (keychain)
  • Discord Integration: Smart detection logic automatically associates Discord signers with their embedded wallets (connection.ts:217-267)

Code Quality

  • Type Safety: Good TypeScript usage with proper interfaces
  • Error Handling: Consistent error response patterns across wallet operations
  • Clean API: The useSocialAuthentication hook provides a simple interface

🔴 Critical Security Concerns

1. Nonce Validation Bypass (turnkey_utils.ts:94-96)

const decodedToken = jwtDecode<DecodedIdToken>(oidcTokenString);
if (!decodedToken.tknonce) {
  return undefined; // ⚠️ CRITICAL: Silently bypasses nonce validation!
}

Risk: If tknonce is missing from JWT, validation is bypassed entirely, potentially allowing replay attacks.

Fix: Should throw an error instead of returning undefined:

if (!decodedToken.tknonce) {
  throw new Error("Missing nonce in token - potential security risk");
}

2. Promise Timeout Implementation (turnkey.ts:363-371)

const timeoutId = setTimeout(() => {
  throw new Error("Timeout waiting for promise"); // ⚠️ BUG: Doesn't reject promise
}, timeoutMs);

Risk: Throwing error in setTimeout callback doesn't reject the promise, leading to hanging promises.

Fix: Use Promise.race pattern:

private async getPromiseResult<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
  const timeoutPromise = new Promise<never>((_, reject) => {
    setTimeout(() => reject(new Error("Timeout waiting for promise")), timeoutMs);
  });
  return Promise.race([promise, timeoutPromise]);
}

3. Popup Window Security (turnkey.ts:390-396)

const openPopup = (url: string) => {
  return window.open(url, "auth0:authorize:popup", `resizable,scrollbars=no,status=1`);
};

Risk: No validation that popup opened successfully, creating potential for clickjacking.

Fix: Add validation:

const openPopup = (url: string) => {
  const popup = window.open(url, "auth0:authorize:popup", `resizable,scrollbars=no,status=1`);
  if (!popup || popup.closed) {
    throw new Error("Failed to open authentication popup - may be blocked");
  }
  return popup;
};

4. Address Validation Risk (turnkey.ts:147-149)

if ("address" in signupOrLogin && BigInt(signupOrLogin.address) !== BigInt(turnkeyAddress)) {
  throw new Error("Account mismatch");
}

Risk: BigInt() constructor can throw for invalid input, causing unexpected errors.

🐛 Potential Bugs

1. Memory Leak Risk (wallets.tsx:66-87)

const intervalId = setInterval(async () => {
  const detected = await parent.externalDetectWallets();
  // ... processing
}, 300);

Risk: If component unmounts during async operation, interval continues running.

Fix: Add proper cleanup:

useEffect(() => {
  let mounted = true;
  const intervalId = setInterval(async () => {
    if (!mounted) return;
    const detected = await parent.externalDetectWallets();
    if (mounted) {
      // ... processing
    }
  }, 300);
  
  return () => {
    mounted = false;
    clearInterval(intervalId);
  };
}, [parent, wallets]);

2. DOM Manipulation Issues (turnkey.ts:38-49)

const turnkeyIframe = document.getElementById("turnkey-iframe-container");
if (turnkeyIframe) {
  document.body.removeChild(turnkeyIframe); // Direct DOM manipulation
}

Risk: Direct DOM manipulation outside React's lifecycle could cause conflicts.

Performance Issues

Aggressive Polling

  • 300ms polling interval (wallets.tsx:85) may be too frequent for production
  • 200ms iframe polling (turnkey.ts:316) adds additional load
  • No cleanup for embeddedWalletsByAddress Map - grows indefinitely

Recommendations:

// Use exponential backoff
const INITIAL_POLL_INTERVAL = 300;
const MAX_POLL_INTERVAL = 3000;

// Add map cleanup
private cleanupStaleWallets() {
  // Remove wallets that are no longer connected
}

🧪 Test Coverage Gaps

Missing Critical Tests

  • Authentication flow: No tests for Auth0 + Turnkey integration
  • Security boundaries: Nonce validation, token handling not tested
  • Error scenarios: Network failures, popup blocking, malformed tokens
  • Memory management: Polling cleanup, DOM lifecycle
  • Discord detection: Automatic wallet linking logic

Recommended Test Coverage

describe('TurnkeyWallet', () => {
  it('should reject missing nonce in JWT token');
  it('should handle popup blocking gracefully');
  it('should cleanup DOM elements on destruction');
  it('should timeout promises properly');
  it('should handle network failures during authentication');
});

describe('Discord Integration', () => {
  it('should detect existing Discord signers');
  it('should link signers to embedded wallets');
});

🔧 Code Quality Improvements

Extract Configuration Constants

const CONFIG = {
  TURNKEY_TIMEOUT_MS: 10_000,
  WALLET_POLL_INTERVAL_MS: 300,
  IFRAME_POLL_INTERVAL_MS: 200,
} as const;

Simplify Complex Methods

  • TurnkeyWallet.connect() is 86 lines - consider breaking into smaller methods
  • pollIframePublicKey() could use async/await patterns more consistently

📋 Action Items

High Priority (Security & Bugs)

  1. Fix nonce validation bypass - throw error instead of returning undefined
  2. Fix promise timeout implementation - use Promise.race pattern
  3. Add popup validation - check if popup opened successfully
  4. Add memory leak prevention - proper cleanup for polling intervals

Medium Priority (Performance & Reliability)

  1. Implement exponential backoff for polling operations
  2. Add wallet map cleanup to prevent memory growth
  3. Extract configuration constants from hardcoded values
  4. Add comprehensive error boundaries for authentication flows

Low Priority (Quality)

  1. Add JSDoc documentation for complex authentication flows
  2. Consider event-based wallet detection instead of polling
  3. Standardize error message formats

🎯 Overall Assessment

This is a solid architectural improvement that enhances security by moving sensitive operations to the keychain iframe. The Discord detection logic is well-implemented and addresses the core session issue.

However, the critical security vulnerabilities around nonce validation and promise handling must be addressed before merging. The memory management issues with polling and the lack of test coverage for authentication flows are also significant concerns.

Recommendation: Request Changes - Address security vulnerabilities and implement proper cleanup mechanisms before merging.


@edisontim edisontim changed the title fix: discord bug no session fix: discord bug because no wallet when there's no session Jun 18, 2025
@claude
Copy link
Contributor

claude bot commented Jun 23, 2025

Claude finished @edisontim's task —— View job


PR Review: Discord Session Bug Fix

Todo List:

  • Check existing reviews and comments to avoid redundancy
  • Create pending review
  • Get PR diff to understand code changes
  • Analyze code changes for security, performance, and quality issues
  • Add inline comments for specific issues found
  • Submit review with concise summary

✅ Review Complete

Successfully analyzed this Discord session bug fix PR and provided comprehensive feedback through GitHub's review system.

Key Findings:

  • 2 Critical security issues requiring immediate attention (nonce validation bypass, broken promise handling)
  • 5 Medium/Low priority improvements for type safety, performance, and code quality
  • Strong architectural improvements moving Turnkey to keychain for better security isolation
  • Good address normalization with proper checksumming throughout wallet implementations

The PR addresses the core Discord authentication issue effectively, but critical security vulnerabilities must be fixed before merging.


@edisontim edisontim force-pushed the feat/fix-discord-bug-no-session branch from f40fa3b to 351cdf7 Compare June 23, 2025 07:57
@claude
Copy link
Contributor

claude bot commented Jun 23, 2025

Claude finished @edisontim's task —— View job


PR Review: Discord Session Bug Fix

I've conducted a comprehensive review of this TypeScript/React gaming wallet PR focusing on security, code quality, and architecture.

Review Tasks:

  • Check for existing reviews to avoid redundancy
  • Check existing comments for context
  • Get PR diff to understand code changes
  • Analyze code for security and quality issues
  • Provide comprehensive review feedback
  • Submit review with overall summary

Key Changes Identified:

  • E2E Workflow: Added working-directory for Playwright tests
  • Address Checksumming: Improved Ethereum address handling across wallet adapters
  • Turnkey Migration: Moved from controller to keychain package for better security
  • Discord Detection: Added automatic detection of existing Discord signers
  • Social Auth Refactor: Simplified authentication flow with new architecture

Critical Security Issues Found:

🔴 1. Nonce Validation Bypass (turnkey_utils.ts:94-96)

const decodedToken = jwtDecode<DecodedIdToken>(oidcTokenString);
if (!decodedToken.tknonce) {
  return undefined; // ⚠️ CRITICAL: Silently bypasses nonce validation!
}

Issue: Missing nonce allows potential replay attacks
Fix: Should throw error instead of returning undefined

if (!decodedToken.tknonce) {
  throw new Error("Missing nonce in token - potential security risk");
}

🔴 2. Promise Timeout Bug (turnkey.ts:363-371)

const timeoutId = setTimeout(() => {
  throw new Error("Timeout waiting for promise"); // ⚠️ BUG: Doesn't reject promise
}, timeoutMs);

Issue: Throwing in setTimeout doesn't reject the promise
Fix: Use Promise.race pattern:

private async getPromiseResult<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
  const timeoutPromise = new Promise<never>((_, reject) => {
    setTimeout(() => reject(new Error("Timeout waiting for promise")), timeoutMs);
  });
  return Promise.race([promise, timeoutPromise]);
}

🔴 3. BigInt Address Comparison Risk (turnkey.ts:147-149)

if ("address" in signupOrLogin && BigInt(signupOrLogin.address) !== BigInt(turnkeyAddress)) {
  throw new Error("Account mismatch");
}

Issue: BigInt() can throw for invalid input
Fix: Add try-catch or validate input format first

Performance & Memory Issues:

⚠️ 4. Missing Cleanup in Connection Hook (connection.ts:217-267)

The new Discord detection logic doesn't have proper cleanup for async operations. Consider adding AbortController support.

⚠️ 5. DOM Manipulation (turnkey.ts:38-49)

const turnkeyIframe = document.getElementById("turnkey-iframe-container");
if (turnkeyIframe) {
  document.body.removeChild(turnkeyIframe);
}

Issue: Direct DOM manipulation outside React lifecycle could cause conflicts

Code Quality Improvements Needed:

📝 6. Extract Configuration Constants

// Instead of magic numbers throughout codebase
const CONFIG = {
  TURNKEY_TIMEOUT_MS: 10_000,
  WALLET_POLL_INTERVAL_MS: 300,
  IFRAME_POLL_INTERVAL_MS: 200,
} as const;

📝 7. Method Complexity

TurnkeyWallet.connect() is 86 lines - consider breaking into smaller methods for better maintainability.

Positive Changes:

Security Architecture

Moving Turnkey to keychain package is excellent - keeps sensitive operations in sandboxed iframe.

Discord Detection Logic (connection.ts:192-267)

Smart automatic detection of existing Discord signers and linking them to embedded wallets.

Address Normalization

Consistent use of getAddress() from ethers for proper address checksumming across all wallet adapters.

Popup Validation (turnkey.ts:390-396)

Good - the new implementation properly validates popup creation.

Test Coverage Gaps:

Missing Critical Tests:

  • Authentication flow (Auth0 + Turnkey integration)
  • Nonce validation security boundaries
  • Error scenarios (network failures, popup blocking)
  • Discord detection logic
  • Memory cleanup for polling operations

Recommendations:

High Priority (Security):

  1. Fix nonce validation bypass - throw error instead of returning undefined
  2. Fix promise timeout implementation - use Promise.race pattern
  3. Add BigInt input validation - prevent runtime errors from invalid addresses

Medium Priority (Performance & Reliability):

  1. Add cleanup for async operations in connection hook
  2. Extract configuration constants from hardcoded values
  3. Consider breaking down complex methods for better maintainability

Low Priority (Quality):

  1. Add comprehensive test coverage for authentication flows
  2. Add JSDoc documentation for complex authentication methods
  3. Consider event-based detection instead of polling where possible

Overall Assessment:

This is a solid architectural improvement that enhances security by moving sensitive operations to the keychain iframe. The Discord detection logic addresses the core session issue effectively. The address checksumming improvements are excellent and prevent common integration issues.

However, the critical security vulnerabilities around nonce validation and promise handling must be addressed before merging. The architectural benefits are significant, but security cannot be compromised.

Recommendation: Request changes for security fixes, then approve. The foundation is strong but needs security hardening.


Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary

This PR successfully addresses the Discord session bug by refactoring Turnkey wallet integration from controller to keychain package. The architectural change improves security by keeping sensitive operations in the sandboxed iframe environment.

Strengths

  • Better Security Architecture: Moving Turnkey to keychain package keeps sensitive operations isolated
  • Improved Address Handling: Proper checksumming with getAddress() across wallet implementations
  • Smart Discord Detection: Automatic linking of existing Discord signers to embedded wallets
  • Code Organization: Cleaner separation between external and embedded wallet management

🔴 Critical Issues

  • Nonce validation bypass in turnkey_utils.ts - silent failure allows potential replay attacks
  • Broken promise timeout in turnkey.ts - setTimeout doesn't reject promises, causing memory leaks

🟡 Medium Priority Issues

  • Missing popup validation - no error handling for blocked authentication popups
  • Unsafe BigInt conversion - can throw for invalid address formats
  • Uncontrolled async operations - potential memory leaks in connection hook

📋 Recommendations

  1. Fix critical security issues before merging (nonce validation, promise handling)
  2. Add proper error boundaries for authentication flows
  3. Extract configuration constants from magic numbers
  4. Add comprehensive tests for authentication edge cases

The refactoring is solid overall, but the critical security vulnerabilities need immediate attention.


const decodedToken = jwtDecode<DecodedIdToken>(oidcTokenString);
if (!decodedToken.tknonce) {
return undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical Security Issue: Nonce validation bypass vulnerability

When tknonce is missing from the JWT token, this silently returns undefined instead of throwing an error. This bypasses nonce validation entirely and could allow replay attacks.

Suggested change
return undefined;
const decodedToken = jwtDecode<DecodedIdToken>(oidcTokenString);
if (!decodedToken.tknonce) {
throw new Error("Missing nonce in token - potential security risk");
}

Risk: Replay attacks, authentication bypass
Priority: High - Fix before merging

throw new Error("Timeout waiting for promise");
}, timeoutMs);

const result = await promise;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical Bug: Promise timeout implementation is broken

Throwing an error in a setTimeout callback doesn't reject the promise, leading to hanging promises that never resolve or reject.

Suggested change
const result = await promise;
private async getPromiseResult<T>(
promise: Promise<T>,
timeoutMs: number,
): Promise<T> {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error("Timeout waiting for promise")), timeoutMs);
});
return Promise.race([promise, timeoutPromise]);
}

Impact: Memory leaks, hanging operations
Priority: High - Fix before merging

return iframePublicKey;
};

const openPopup = (url: string) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Security Enhancement: Add popup validation

The popup could be blocked by browsers or fail to open, but there's no validation. This could create a poor user experience or potential security issues.

Suggested change
const openPopup = (url: string) => {
const openPopup = (url: string) => {
const popup = window.open(
url,
"auth0:authorize:popup",
`resizable,scrollbars=no,status=1`,
);
if (!popup || popup.closed) {
throw new Error("Failed to open authentication popup - may be blocked");
}
return popup;
};

Impact: Better error handling, prevents silent failures
Priority: Medium

turnkeyIframeClient!,
);
if (
"address" in signupOrLogin &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Type Safety Issue: Unsafe BigInt conversion

The BigInt() constructor can throw for invalid input, causing unexpected errors. Add error handling or validation.

Suggested change
"address" in signupOrLogin &&
if (
"address" in signupOrLogin &&
(() => {
try {
return BigInt(signupOrLogin.address) !== BigInt(turnkeyAddress);
} catch (error) {
throw new Error(`Invalid address format: ${signupOrLogin.address}`);
}
})()
) {
throw new Error("Account mismatch");
}

Impact: Prevents runtime errors from invalid address formats
Priority: Medium

}, [rpcUrl]);

useEffect(() => {
if (!controller?.username() || !chainId) return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Performance Concern: Uncontrolled async operation

This async IIFE could continue running even after the component unmounts, potentially causing memory leaks or state updates on unmounted components.

Suggested change
if (!controller?.username() || !chainId) return;
useEffect(() => {
if (!controller?.username() || !chainId) return;
const abortController = new AbortController();
(async () => {
try {
if (abortController.signal.aborted) return;
const controllerResponse = await fetchController(
chainId,
controller.username(),
abortController.signal,
);
// ... rest of the logic with abortController.signal checks
} catch (error) {
if (error.name === 'AbortError') return;
console.error("Failed to add embedded wallet:", error);
}
})();
return () => {
abortController.abort();
};
}, [controller?.username, chainId]);

Impact: Prevents memory leaks and race conditions
Priority: Medium


try {
const turnkeyIframeClient = await this.getTurnkeyIframeClient(10_000);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Code Quality: Extract magic numbers to constants

Magic numbers scattered throughout the code make it harder to maintain and configure.

Suggested change
const CONFIG = {
IFRAME_TIMEOUT_MS: 10_000,
IFRAME_POLL_INTERVAL_MS: 200,
IFRAME_POLL_TIMEOUT_MS: 10_000,
} as const;
// Then use: this.pollIframePublicKey(CONFIG.IFRAME_POLL_TIMEOUT_MS)

Impact: Better maintainability and configurability
Priority: Low

clientId: import.meta.env.VITE_AUTH0_CLIENT_ID,
});

const turnkeyIframe = document.getElementById("turnkey-iframe-container");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Resource Management: DOM element cleanup could be improved

Direct DOM manipulation outside React's lifecycle could cause issues. Consider using refs or effect cleanup.

Suggested change
const turnkeyIframe = document.getElementById("turnkey-iframe-container");
constructor() {
this.auth0ClientPromise = createAuth0Client({
domain: import.meta.env.VITE_AUTH0_DOMAIN,
clientId: import.meta.env.VITE_AUTH0_CLIENT_ID,
});
// Cleanup existing iframe
const existingIframe = document.getElementById("turnkey-iframe-container");
if (existingIframe?.parentNode) {
existingIframe.parentNode.removeChild(existingIframe);
}
// ... rest of iframe setup
}

Impact: Prevents potential DOM conflicts
Priority: Low

@edisontim edisontim merged commit 5e43508 into main Jun 23, 2025
20 of 21 checks passed
@edisontim edisontim deleted the feat/fix-discord-bug-no-session branch June 23, 2025 08:47
@claude
Copy link
Contributor

claude bot commented Jun 23, 2025

Claude finished @edisontim's task —— View job


Documentation Analysis for Discord Session Bug Fix

Analyzing PR #1762 changes to determine if documentation updates are needed for the Turnkey wallet refactoring and Discord session fixes.

Todo List:

  • Examine the changed files to understand architectural changes
  • Check docs-repo directory structure and existing documentation
  • Identify user-facing changes that require documentation updates
  • Update documentation files if needed
  • Verify no internal-only changes are being documented unnecessarily

Analysis Complete

After thoroughly analyzing the PR changes and examining the existing documentation, I've determined that no documentation updates are needed for this PR.

Key Findings:

1. Nature of Changes

  • This is primarily an internal architectural refactor
  • Turnkey social authentication moved from packages/controller/src/wallets/turnkey/ to packages/keychain/src/wallets/social/
  • This is a security improvement - moving sensitive operations to the sandboxed keychain iframe
  • Address handling improvements with proper checksumming across wallet adapters

2. User-Facing API Impact

  • The main Controller API remains unchanged - users still call controller.connect()
  • Social authentication (including Discord) is still available and works the same way
  • The authentication flow is handled internally - users don't interact with Turnkey wallets directly
  • No breaking changes to public interfaces

3. Documentation Review

  • Searched existing documentation for references to social authentication, Turnkey, or Discord
  • Current documentation focuses on high-level Controller API usage
  • Documentation doesn't contain implementation details about internal wallet handling
  • No user-facing authentication APIs are documented that would need updating

4. Controller API Consistency

  • Main exports in packages/controller/src/index.ts remain the same
  • Users still use the same ControllerConnector and controller.connect() patterns
  • Configuration options and public methods are unchanged

Conclusion

This PR addresses a Discord session bug through internal architectural improvements without changing the public API or user experience. The existing documentation accurately reflects the current user-facing functionality, and no updates are required.

The refactoring improves security by moving social authentication to the keychain sandbox, but this internal change is transparent to users and doesn't require documentation changes.


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants